其實今天的主題應該算是昨天 High Performance CSS 的延伸。
大家應該都有遇過網頁的動畫有點不順暢甚至卡頓的經驗吧?是不是覺得很煩人呢?就算沒有馬上跳出網頁應該也暗自在心裡把這個網站畫上了幾個叉叉。
今天我們就專注在「如何讓網站達到更順暢的動畫體驗與效能
」這個主題上。
這點在昨天還有 Day08 時都有提過,因為 transform 不會觸發 reflow 與 repaint,所以在效能上會比改變元素的 width, height, position 的 left 或 top 還要好。
阿怎麼同樣的東西要講三次啊!
:因為真的很重要(誤)。
別急,當然不會這樣騙讀者的點閱還有一天的篇幅,今天要介紹的是另一個概念 - Hardware Acceleration 硬體加速。
硬體加速簡單來說就是瀏覽器會把一些比較複雜的頁面渲染相關任務交給 GPU 處理,而不是全部都靠 CPU 來完成。而一些頁面的 animation 就可以透過 GPU 加速使頁面渲染速度更快且更加流暢。
GPU 圖形處理器,全名為 Graphics Processing Unit
,是一種專門用在執行繪圖運算工作的微處理器。
我們都知道 CPU 在電腦的主機板上,可以說是電腦的「大腦」,而 GPU 則位於電腦的顯示卡上.主要用途為圖形的渲染。而 GPU 是專門為了執行圖形渲染所需要的複雜數學與幾何計算而生的,所以把一些複雜的操作交給 GPU 處理,除了性能上的顯著提升外,也減輕了 CPU 的工作壓力,這效果在 mobile 的設備上尤其明顯。
還記得在 Day08 介紹瀏覽器架構與渲染流程時有提過 Layer 分層嗎?其實 GPU 硬體加入的原理就跟分層有關。當頁面上的 element 執行了某些操作,例如說 3D 的 transform,該元素會被提升到一個單獨的圖層,獨立於頁面上的其他部分,並在後期做之前講過的柵格化與合成並繪製到螢幕上。
把元素提升到自己的圖層的好處是當頁面上只有這個元素要做 transform 時,其他的元素不需要重新渲染,這使得效能帶來極大的優化。
此外還記得 Day08 時提過的現代瀏覽器架構嗎?
GPU 會運行在獨立的 Process 裡,不會 block 住 Render Engine 的其他工作,現在是不是更了解瀏覽器性能提升所帶來的影響了啊?
就算 CSS 寫得再好,如果瀏覽器沒有開啟硬體加速,也是沒辦法享受它的強大的,這邊以常用的 Chrome 為例子:
Chrome 的預設選項是會開啟硬體加速的,不過人生總是有些意外,建議還是去看看有沒有不小心被關掉了喔!
再來可以在網址列輸入 chrome://gpu 看看目前你的 Chrome 瀏覽器中有哪些功能是開啟 GPU 加速的
有一些 CSS 屬性被歸類於 GPU accelerated properties,也就是被瀏覽器認定為「有機會」被 GPU 加速的屬性:
transform
filter
opacity
所以說只要使用這些屬性並且瀏覽器有啟用硬體加速就會自動使用 GPU 加速了嗎?
答案是否定的。
使用這些屬性並不保證會被 GPU 加速,一般來說這些工作還是由瀏覽器的渲染引擎來執行的,不過有幾個例外,例如如果是 2D 的 transform 就不會開啟硬體加速,而 3D 的 transform 如果瀏覽器有啟用的話則會強制使用硬體加速操作。
3D transform ? 原來三維空間的操作可以透過硬體加速呀!可是...我好像沒有在寫什麼 3D 的動畫,都是二維的簡單動畫,看來...我跟硬體加速還是無緣嗎...?
那可不一定,雖然說 3D transform 才能透過硬體加速,但我們可以走一點偷吃步的方式,讓二維的變化假扮成是三維的操作,這種偷吃步的方法又被稱作 transform hack,而通常會使用的屬性有 transformZ 或與 translate3D。
例如你原本預期的動畫是這樣:
transform: translate(0, 20px);
讓元素向下移動 20px,你可以使用以下的方法來開啟硬體加速
transform: translate3D(0, 20px, 0);
使用這樣的方式會創建一個合成層,這個合成層會被送到 GPU 並由 GPU 來合成。
看到這裡我知道你在偷笑了。
那我以後都用 transform hack 的方式來開啟 GPU 加速就好啦!效能提升好棒棒!
怎麼到今天你還沒有學乖咧,看起來美好的事物通常都是一把雙面刃的!
建立一個獨立的圖層確實可以提高渲染效能,但是它帶來的缺點就是會讓 memory 的使用量急遽提升,這種狀況在 mobile device 上尤其可怕,一不注意可能就會導致手機的瀏覽器直接 crash。所以在使用這種方式時要格外小心,必須在非常確定這麼做可以提升頁面性能時再使用,不然真的會得不償失,反而讓效能出現瓶頸!
後來出現了一個新的 CSS 屬性 - will-change,讓我們可以不必再依賴 transform hack 的方式來開啟硬體加速。
will-change 屬性比較像是一個 hint,讓你提前告訴瀏覽器你可能會在未來對元素做什麼類型的操作,讓瀏覽器可以提前做準備,使渲染流程更快速與順暢。
例如說,我們提過 CSS transform 有機會把元素提升到一個新的圖層,進行合成後再渲染到螢幕上,不過提升一個圖層其實是耗性能的操作,有可能讓 transform 的動畫延遲開始,造成螢幕的閃爍。
要解決這個問題的其中一個方法就是提早通知瀏覽器讓它做好準備,等到真的需要時就可以迅速執行。will-change 就可以做到這件事:
will-change: transform;
/* 也可以一次給多個 */
will-change: transform, opacity;
關於 will-change 有哪些屬性可以帶,可以參考這裡。
上面的例子是告訴瀏覽器這個元素未來會做 transform,讓瀏覽器可以先行準備並「讓瀏覽器選擇
」最好的方式來處理這個變動。相較於 hack transform 強制創建可能對效能沒有幫助的圖層並開啟硬體加速,will-change 無疑是更好的選擇。
例如
* {
will-change: transform;
}
這樣的寫法告訴瀏覽器什麼? 「所有頁面上的屬性都有可能發生變化,有機會的話幫我優化喔!」這樣其實有講就等於沒講一樣,因為完全看不出任何的優先層級,而且這樣瀏覽器還需要花計算資源處理 will-change,過度使用反而有機會讓頁面效能直接炸裂的!(稍後會有炸裂的 dmeo,慎入,真的是直接炸裂XDD)
瀏覽器對 will-change 提示進行的優化通常有很高的成本,會佔用大量的資源,如果是一個不會一直觸發 transform 的元素,在變更生效後建議先透過 JavaScript 動態移除 will-change 屬性以釋放資源。
例如,監聽元素的 animationEnd 事件在動畫結束後移除 will-change 的屬性。
let el = document.getElementById('puddydat');
// Set will-change when the user hovers over the element
// then remove the hint once the animation has ended
el.addEventListener('mouseenter', addHint);
el.addEventListener('animationEnd', removeHint);
// Function to set the hint
function addHint() {
this.style.willChange = 'transform';
}
// Function to remove the hint
function removeHint() {
this.style.willChange = 'auto';
}
如果元素需要不停的觸發動畫,例如這個例子 中的紅色移動球體,就很適合把 will-change 留在上面。想到的另一個例子是跟隨使用者滑鼠鼠標移動的動畫,因為可以預期元素會有規律且頻繁的變化,所以保持優化狀態應該是合理的。
前面講了那麼多理論,但我知道人總是比較容易相信親眼見過的事實,那就來個 demo 吧。
我在這個 demo 中渲染出了非常大量的表格元素,並隨機給予區分大小寫的英文字母,上面會有一個按鈕,點擊後會把元素作顛倒的排序(reverse),在重新排列時會顯示一個漸變的動畫。
我分成兩種做法,第一種是比較糟糕的做法,排列時直接修改元素的 style.top
document.querySelector('#reorderBtn').addEventListener('click', function () {
elementRows.reverse();
elementRows.forEach(function (eRow, rowIndex) {
eRow.style.top = rowIndex * ROW_HEIGHT + 'px';
});
});
跑起來的結果是這樣
第二種方式則是用比較推薦的 transform: translate,並且加上了剛剛提的 will-change
.row {
height: 25px;
box-sizing: border-box;
position: absolute;
width: 100%;
will-change: transofrm;
transition: transform 0.5s;
}
document.querySelector('#reorderBtn').addEventListener('click', function () {
elementRows.reverse();
elementRows.forEach(function (elementRow, rowIndex) {
elementRow.style.transform =
'translate(0, ' + rowIndex * ROW_HEIGHT + 'px)';
});
});
跑起來的結果則是這樣
你可能覺得看起來沒有什麼巨大差別,的確,做成 gif 圖檔後看起來沒有差很多,所以我決定把 demo 做個簡單部署,讓各位親自體會一下兩種動畫實作方式的效能差異。(強烈建議讀者點開來感受一下差異,因為只是簡單部署到 AWS S3,就沒有另外去設置 https 了,還請各位見諒。)
自己覺得差異非常非常的顯著,不過為了方便 demo,我是把 will-change 屬性一直留在元素上的,這個 case 應該可以在 reorder 後移除 will-change 才對,不過看起來成效還是不錯的,動畫相較於原本寫法更為順暢了。你也跟我ㄧ樣被嚇到了嗎?
Demo Source Code: https://github.com/kylemocode/it-ironman-2021/tree/master/css-transform-demo
話說,記得前面提過不要濫用 will-change 嗎?給各位看看濫用的下場是什麼。我僅僅是在 CSS 加入一行:
* {
will-change: transform;
}
我們來看看頁面變成什麼樣子(非常可怕,慎入!)
http://very-bad-animation-demo.s3-website-us-east-1.amazonaws.com/
看起來甚至比第一種方式還更糟了,光初頻的渲染就要花上一段時間了呢!
恭喜你們學會用一行 CSS 就能摧毀一個網站的技能(誤?)
我自己蠻喜歡今天的主題的,動畫在現在的網頁幾乎是不可或缺的 feature,如何讓動畫保持順暢讓使用者有良好的體驗是前端開發者不能忽視的難題。有趣的是動畫的卡頓未必會反應在 Lighthouse 等檢測工具的分數上,這就是我在 Day03 說過的「不是只有會反應在分數上的才是做效能優化要關注的點。」
自己也覺得最後三種版本的 demo 還蠻有趣的,僅僅改了一兩行 code 居然會讓動畫效能有那麼大的差異,希望經過昨天與今天的內容,能夠讓你對於寫出效能更好的 CSS 更有自信。
Delivery Optimizations & Render Process Optimizations 這個篇章也告一段落囉,明天將進入 Build Optimizations 篇章,不知道各位還在嗎?? 請撐住!
我們明天見!
https://www.sitepoint.com/introduction-to-hardware-acceleration-css-animations/
https://developers.google.com/web/updates/2019/08/get-started-with-gpu-compute-on-the-web
https://www.maxlaumeister.com/articles/css-will-change-property-a-performance-case-study/
https://www.quackit.com/css/css3/properties/css_will-change.cfm